# Copyright (c) 2012 MaxMedia and Travis Warlick
# Licensed under the MIT License (see LICENSE)

require 'active_support/concern'
require 'active_support/callbacks'
require 'dionysus/array/assert_types'

module Dionysus
  module Decorator
    def extend_object(base)
      if base.class == Class or base.class == Module
        super
      else
        _decorations.setup(base)
        if const_defined?("Methods")
          base.send :extend, const_get("Methods")
        end
      end
    end

    def before(*args, &block)
      name, options, block = callback_args(*(args + [block]))
      _decorations.add(name, :before, options || {}, block)
    end

    def after(*args, &block)
      name, options, block = callback_args(*(args + [block]))
      _decorations.add(name, :after, options || {}, block)
    end

    # TODO implement around callback.  Currently breaks with: no block given (yield) (LocalJumpError)
    # def around(*args, &block)
    #   name, options, block = callback_args(*(args + [block]))
    #   _decorations.add(name, :around, options || {}, block)
    # end

    def decorate(&block)
      _decorations.add block
    end

    private

    def callback_args(*args)
      args = args.compact

      name = args.shift

      block = args.detect {|obj| obj.is_a?(Proc)}
      args.delete(block) if block

      options = args.detect {|obj| obj.is_a?(Hash)}
      args.delete(options) if options

      if args.length > 0
        raise ArgumentError, "Too many arguments"
      end

      return name, options, block
    end

    def _decorations
      @_decorations ||= Decorations.new(self)
    end

    class Decorations
      def initialize(mod)
        @module = mod
        @callbacks = {}
        @blocks = []
      end

      def add(*args)
        if args.length == 1 and args[0].is_a?(Proc)
          @blocks << args.first
        else
          name = args.shift.to_sym
          args.assert_types(Symbol, Hash, Proc)
          (@callbacks[name] ||= []).tap do |array|
            array << Callback.new(*args)
          end
        end
      end

      def setup(obj)
        setup_callbacks(obj)
        obj.extend build_decorator_module
        @blocks.each { |b| obj.instance_eval(&b) }
      end
 
      private

      def setup_callbacks(obj)
        obj.class.define_callbacks *@callbacks.keys
        @callbacks.each do |name, callbacks|
          callbacks.each { |cb| cb.setup(obj, name) }
        end
      end

      def build_decorator_module
        Module.new.tap do |result|
          @callbacks.keys.each do |name|
            result.module_eval(<<-EOS, __FILE__, __LINE__ + 1)
              def #{name}(*args, &block)
                run_callbacks(#{name.to_sym.inspect}) do
                  super(*args, &block)
                end
              end
            EOS
          end
        end
      end
 
      class Callback < Struct.new(:type, :options, :block)
        def setup(obj, name)
          obj.class.set_callback(name, type, options, &block)
        end
      end
    end
  end

  module Decoratable
    extend ActiveSupport::Concern
    include ActiveSupport::Callbacks

    def decorate_with(mod)
      # self is Object decendant instance (which includes Decoratable)
      # TODO verify mod extends the Decorator module
      if mod.is_a?(Class)
        mod.extend_object(self)
      elsif mod.is_a?(Module)
        self.extend(mod)
      else
        raise ArgumentError, "Wrong type %p.  Expected extension of Dionysus::Decorator"%[mod.class]
      end
    end
    alias_method :with, :decorate_with
  end
end